Convex Agents
Build persistent, stateful AI agents with Convex including thread management, tool integration, streaming responses, RAG patterns, and workflow orchestration.
Documentation Sources
Before implementing, do not assume; fetch the latest documentation:
Primary:
https://docs.convex.dev/ai
Convex Agent Component:
https://www.npmjs.com/package/@convex-dev/agent
For broader context:
https://docs.convex.dev/llms.txt
Instructions
Why Convex for AI Agents
Persistent State
- Conversation history survives restarts
Real-time Updates
- Stream responses to clients automatically
Tool Execution
- Run Convex functions as agent tools
Durable Workflows
- Long-running agent tasks with reliability
Built-in RAG
- Vector search for knowledge retrieval
Setting Up Convex Agent
npm
install
@convex-dev/agent ai openai
// convex/agent.ts
import
{
Agent
}
from
"@convex-dev/agent"
;
import
{
components
}
from
"./_generated/api"
;
import
{
OpenAI
}
from
"openai"
;
const
openai
=
new
OpenAI
(
)
;
export
const
agent
=
new
Agent
(
components
.
agent
,
{
chat
:
openai
.
chat
,
textEmbedding
:
openai
.
embeddings
,
}
)
;
Thread Management
// convex/threads.ts
import
{
mutation
,
query
}
from
"./_generated/server"
;
import
{
v
}
from
"convex/values"
;
import
{
agent
}
from
"./agent"
;
// Create a new conversation thread
export
const
createThread
=
mutation
(
{
args
:
{
userId
:
v
.
id
(
"users"
)
,
title
:
v
.
optional
(
v
.
string
(
)
)
,
}
,
returns
:
v
.
id
(
"threads"
)
,
handler
:
async
(
ctx
,
args
)
=>
{
const
threadId
=
await
agent
.
createThread
(
ctx
,
{
userId
:
args
.
userId
,
metadata
:
{
title
:
args
.
title
??
"New Conversation"
,
createdAt
:
Date
.
now
(
)
,
}
,
}
)
;
return
threadId
;
}
,
}
)
;
// List user's threads
export
const
listThreads
=
query
(
{
args
:
{
userId
:
v
.
id
(
"users"
)
}
,
returns
:
v
.
array
(
v
.
object
(
{
_id
:
v
.
id
(
"threads"
)
,
title
:
v
.
string
(
)
,
lastMessageAt
:
v
.
optional
(
v
.
number
(
)
)
,
}
)
)
,
handler
:
async
(
ctx
,
args
)
=>
{
return
await
agent
.
listThreads
(
ctx
,
{
userId
:
args
.
userId
,
}
)
;
}
,
}
)
;
// Get thread messages
export
const
getMessages
=
query
(
{
args
:
{
threadId
:
v
.
id
(
"threads"
)
}
,
returns
:
v
.
array
(
v
.
object
(
{
role
:
v
.
string
(
)
,
content
:
v
.
string
(
)
,
createdAt
:
v
.
number
(
)
,
}
)
)
,
handler
:
async
(
ctx
,
args
)
=>
{
return
await
agent
.
getMessages
(
ctx
,
{
threadId
:
args
.
threadId
,
}
)
;
}
,
}
)
;
Sending Messages and Streaming Responses
// convex/chat.ts
import
{
action
}
from
"./_generated/server"
;
import
{
v
}
from
"convex/values"
;
import
{
agent
}
from
"./agent"
;
import
{
internal
}
from
"./_generated/api"
;
export
const
sendMessage
=
action
(
{
args
:
{
threadId
:
v
.
id
(
"threads"
)
,
message
:
v
.
string
(
)
,
}
,
returns
:
v
.
null
(
)
,
handler
:
async
(
ctx
,
args
)
=>
{
// Add user message to thread
await
ctx
.
runMutation
(
internal
.
chat
.
addUserMessage
,
{
threadId
:
args
.
threadId
,
content
:
args
.
message
,
}
)
;
// Generate AI response with streaming
const
response
=
await
agent
.
chat
(
ctx
,
{
threadId
:
args
.
threadId
,
messages
:
[
{
role
:
"user"
,
content
:
args
.
message
}
]
,
stream
:
true
,
onToken
:
async
(
token
)
=>
{
// Stream tokens to client via mutation
await
ctx
.
runMutation
(
internal
.
chat
.
appendToken
,
{
threadId
:
args
.
threadId
,
token
,
}
)
;
}
,
}
)
;
// Save complete response
await
ctx
.
runMutation
(
internal
.
chat
.
saveResponse
,
{
threadId
:
args
.
threadId
,
content
:
response
.
content
,
}
)
;
return
null
;
}
,
}
)
;
Tool Integration
Define tools that agents can use:
// convex/tools.ts
import
{
tool
}
from
"@convex-dev/agent"
;
import
{
v
}
from
"convex/values"
;
import
{
api
}
from
"./_generated/api"
;
// Tool to search knowledge base
export
const
searchKnowledge
=
tool
(
{
name
:
"search_knowledge"
,
description
:
"Search the knowledge base for relevant information"
,
parameters
:
v
.
object
(
{
query
:
v
.
string
(
)
,
limit
:
v
.
optional
(
v
.
number
(
)
)
,
}
)
,
handler
:
async
(
ctx
,
args
)
=>
{
const
results
=
await
ctx
.
runQuery
(
api
.
knowledge
.
search
,
{
query
:
args
.
query
,
limit
:
args
.
limit
??
5
,
}
)
;
return
results
;
}
,
}
)
;
// Tool to create a task
export
const
createTask
=
tool
(
{
name
:
"create_task"
,
description
:
"Create a new task for the user"
,
parameters
:
v
.
object
(
{
title
:
v
.
string
(
)
,
description
:
v
.
optional
(
v
.
string
(
)
)
,
dueDate
:
v
.
optional
(
v
.
string
(
)
)
,
}
)
,
handler
:
async
(
ctx
,
args
)
=>
{
const
taskId
=
await
ctx
.
runMutation
(
api
.
tasks
.
create
,
{
title
:
args
.
title
,
description
:
args
.
description
,
dueDate
:
args
.
dueDate
?
new
Date
(
args
.
dueDate
)
.
getTime
(
)
:
undefined
,
}
)
;
return
{
success
:
true
,
taskId
}
;
}
,
}
)
;
// Tool to get weather
export
const
getWeather
=
tool
(
{
name
:
"get_weather"
,
description
:
"Get current weather for a location"
,
parameters
:
v
.
object
(
{
location
:
v
.
string
(
)
,
}
)
,
handler
:
async
(
ctx
,
args
)
=>
{
const
response
=
await
fetch
(
https://api.weather.com/current?location=
${
encodeURIComponent
(
args
.
location
)
}
)
;
return
await
response
.
json
(
)
;
}
,
}
)
;
Agent with Tools
// convex/assistant.ts
import
{
action
}
from
"./_generated/server"
;
import
{
v
}
from
"convex/values"
;
import
{
agent
}
from
"./agent"
;
import
{
searchKnowledge
,
createTask
,
getWeather
}
from
"./tools"
;
export
const
chat
=
action
(
{
args
:
{
threadId
:
v
.
id
(
"threads"
)
,
message
:
v
.
string
(
)
,
}
,
returns
:
v
.
string
(
)
,
handler
:
async
(
ctx
,
args
)
=>
{
const
response
=
await
agent
.
chat
(
ctx
,
{
threadId
:
args
.
threadId
,
messages
:
[
{
role
:
"user"
,
content
:
args
.
message
}
]
,
tools
:
[
searchKnowledge
,
createTask
,
getWeather
]
,
systemPrompt
:
You are a helpful assistant. You have access to tools to:
- Search the knowledge base for information
- Create tasks for the user
- Get weather information
Use these tools when appropriate to help the user.
,
}
)
;
return
response
.
content
;
}
,
}
)
;
RAG (Retrieval Augmented Generation)
// convex/knowledge.ts
import
{
mutation
,
query
}
from
"./_generated/server"
;
import
{
v
}
from
"convex/values"
;
import
{
agent
}
from
"./agent"
;
// Add document to knowledge base
export
const
addDocument
=
mutation
(
{
args
:
{
title
:
v
.
string
(
)
,
content
:
v
.
string
(
)
,
metadata
:
v
.
optional
(
v
.
object
(
{
source
:
v
.
optional
(
v
.
string
(
)
)
,
category
:
v
.
optional
(
v
.
string
(
)
)
,
}
)
)
,
}
,
returns
:
v
.
id
(
"documents"
)
,
handler
:
async
(
ctx
,
args
)
=>
{
// Generate embedding
const
embedding
=
await
agent
.
embed
(
ctx
,
args
.
content
)
;
return
await
ctx
.
db
.
insert
(
"documents"
,
{
title
:
args
.
title
,
content
:
args
.
content
,
embedding
,
metadata
:
args
.
metadata
??
{
}
,
createdAt
:
Date
.
now
(
)
,
}
)
;
}
,
}
)
;
// Search knowledge base
export
const
search
=
query
(
{
args
:
{
query
:
v
.
string
(
)
,
limit
:
v
.
optional
(
v
.
number
(
)
)
,
}
,
returns
:
v
.
array
(
v
.
object
(
{
_id
:
v
.
id
(
"documents"
)
,
title
:
v
.
string
(
)
,
content
:
v
.
string
(
)
,
score
:
v
.
number
(
)
,
}
)
)
,
handler
:
async
(
ctx
,
args
)
=>
{
const
results
=
await
agent
.
search
(
ctx
,
{
query
:
args
.
query
,
table
:
"documents"
,
limit
:
args
.
limit
??
5
,
}
)
;
return
results
.
map
(
(
r
)
=>
(
{
_id
:
r
.
_id
,
title
:
r
.
title
,
content
:
r
.
content
,
score
:
r
.
_score
,
}
)
)
;
}
,
}
)
;
Workflow Orchestration
// convex/workflows.ts
import
{
action
,
internalMutation
}
from
"./_generated/server"
;
import
{
v
}
from
"convex/values"
;
import
{
agent
}
from
"./agent"
;
import
{
internal
}
from
"./_generated/api"
;
// Multi-step research workflow
export
const
researchTopic
=
action
(
{
args
:
{
topic
:
v
.
string
(
)
,
userId
:
v
.
id
(
"users"
)
,
}
,
returns
:
v
.
id
(
"research"
)
,
handler
:
async
(
ctx
,
args
)
=>
{
// Create research record
const
researchId
=
await
ctx
.
runMutation
(
internal
.
workflows
.
createResearch
,
{
topic
:
args
.
topic
,
userId
:
args
.
userId
,
status
:
"searching"
,
}
)
;
// Step 1: Search for relevant documents
const
searchResults
=
await
agent
.
search
(
ctx
,
{
query
:
args
.
topic
,
table
:
"documents"
,
limit
:
10
,
}
)
;
await
ctx
.
runMutation
(
internal
.
workflows
.
updateStatus
,
{
researchId
,
status
:
"analyzing"
,
}
)
;
// Step 2: Analyze and synthesize
const
analysis
=
await
agent
.
chat
(
ctx
,
{
messages
:
[
{
role
:
"user"
,
content
:
Analyze these sources about "
${
args
.
topic
}
" and provide a comprehensive summary:\n\n
${
searchResults
.
map
(
(
r
)
=>
r
.
content
)
.
join
(
"\n\n---\n\n"
)
}
,
}
]
,
systemPrompt
:
"You are a research assistant. Provide thorough, well-cited analysis."
,
}
)
;
// Step 3: Generate key insights
await
ctx
.
runMutation
(
internal
.
workflows
.
updateStatus
,
{
researchId
,
status
:
"summarizing"
,
}
)
;
const
insights
=
await
agent
.
chat
(
ctx
,
{
messages
:
[
{
role
:
"user"
,
content
:
Based on this analysis, list 5 key insights:\n\n
${
analysis
.
content
}
,
}
]
,
}
)
;
// Save final results
await
ctx
.
runMutation
(
internal
.
workflows
.
completeResearch
,
{
researchId
,
analysis
:
analysis
.
content
,
insights
:
insights
.
content
,
sources
:
searchResults
.
map
(
(
r
)
=>
r
.
_id
)
,
}
)
;
return
researchId
;
}
,
}
)
;
Examples
Complete Chat Application Schema
// convex/schema.ts
import
{
defineSchema
,
defineTable
}
from
"convex/server"
;
import
{
v
}
from
"convex/values"
;
export
default
defineSchema
(
{
threads
:
defineTable
(
{
userId
:
v
.
id
(
"users"
)
,
title
:
v
.
string
(
)
,
lastMessageAt
:
v
.
optional
(
v
.
number
(
)
)
,
metadata
:
v
.
optional
(
v
.
any
(
)
)
,
}
)
.
index
(
"by_user"
,
[
"userId"
]
)
,
messages
:
defineTable
(
{
threadId
:
v
.
id
(
"threads"
)
,
role
:
v
.
union
(
v
.
literal
(
"user"
)
,
v
.
literal
(
"assistant"
)
,
v
.
literal
(
"system"
)
)
,
content
:
v
.
string
(
)
,
toolCalls
:
v
.
optional
(
v
.
array
(
v
.
object
(
{
name
:
v
.
string
(
)
,
arguments
:
v
.
any
(
)
,
result
:
v
.
optional
(
v
.
any
(
)
)
,
}
)
)
)
,
createdAt
:
v
.
number
(
)
,
}
)
.
index
(
"by_thread"
,
[
"threadId"
]
)
,
documents
:
defineTable
(
{
title
:
v
.
string
(
)
,
content
:
v
.
string
(
)
,
embedding
:
v
.
array
(
v
.
float64
(
)
)
,
metadata
:
v
.
object
(
{
source
:
v
.
optional
(
v
.
string
(
)
)
,
category
:
v
.
optional
(
v
.
string
(
)
)
,
}
)
,
createdAt
:
v
.
number
(
)
,
}
)
.
vectorIndex
(
"by_embedding"
,
{
vectorField
:
"embedding"
,
dimensions
:
1536
,
}
)
,
}
)
;
React Chat Component
import
{
useQuery
,
useMutation
,
useAction
}
from
"convex/react"
;
import
{
api
}
from
"../convex/_generated/api"
;
import
{
useState
,
useRef
,
useEffect
}
from
"react"
;
function
ChatInterface
(
{
threadId
}
:
{
threadId
:
Id
<
"threads"
} ) { const messages = useQuery ( api . threads . getMessages , { threadId } ) ; const sendMessage = useAction ( api . chat . sendMessage ) ; const [ input , setInput ] = useState ( "" ) ; const [ sending , setSending ] = useState ( false ) ; const messagesEndRef = useRef < HTMLDivElement
( null ) ; useEffect ( ( ) => { messagesEndRef . current ?. scrollIntoView ( { behavior : "smooth" } ) ; } , [ messages ] ) ; const handleSend = async ( e : React . FormEvent ) => { e . preventDefault ( ) ; if ( ! input . trim ( ) || sending ) return ; const message = input . trim ( ) ; setInput ( "" ) ; setSending ( true ) ; try { await sendMessage ( { threadId , message } ) ; } finally { setSending ( false ) ; } } ; return ( < div className = "chat-container"
< div className = "messages"
{ messages ?. map ( ( msg , i ) => ( < div key = { i } className = {
message ${ msg . role }}< strong
{ msg . role === "user" ? "You" : "Assistant" } : < / strong
< p
{ msg . content } < / p
< / div
) ) } < div ref = { messagesEndRef } /
< / div
< form onSubmit = { handleSend } className = "input-form"
< input value = { input } onChange = { ( e ) => setInput ( e . target . value ) } placeholder = "Type your message..." disabled = { sending } /
< button type = "submit" disabled = { sending || ! input . trim ( ) }
{ sending ? "Sending..." : "Send" } < / button
< / form
< / div
) ; } Best Practices Never run npx convex deploy unless explicitly instructed Never run any git commands unless explicitly instructed Store conversation history in Convex for persistence Use streaming for better user experience with long responses Implement proper error handling for tool failures Use vector indexes for efficient RAG retrieval Rate limit agent interactions to control costs Log tool usage for debugging and analytics Common Pitfalls Not persisting threads - Conversations lost on refresh Blocking on long responses - Use streaming instead Tool errors crashing agents - Add proper error handling Large context windows - Summarize old messages Missing embeddings for RAG - Generate embeddings on insert References Convex Documentation: https://docs.convex.dev/ Convex LLMs.txt: https://docs.convex.dev/llms.txt Convex AI: https://docs.convex.dev/ai Agent Component: https://www.npmjs.com/package/@convex-dev/agent